#ifndef PUDGE_GRFX_SHADER_HPP
#define PUDGE_GRFX_SHADER_HPP

#include <pudge/math/mat.hpp>
#include <pudge/math/vec.hpp>

#include <string>
#include <iostream>
#include <fstream>
#include <sstream>

namespace pudge { namespace grfx {

	void test_gl_error(std::string const& msg)
	{
		GLenum er = glGetError();

		if(er != GL_NO_ERROR)
		{
			switch(er)
			{
			case GL_INVALID_ENUM:
				throw -1;
				break;
			case GL_INVALID_VALUE:
				throw -1;
				break;
			case GL_INVALID_OPERATION:
				throw -1;
				break;
			case GL_STACK_OVERFLOW:
				throw -1;
				break;
			case GL_STACK_UNDERFLOW:
				throw -1;
				break;
			case GL_OUT_OF_MEMORY:
				throw -1;
				break;
			case GL_TABLE_TOO_LARGE:
				throw -1;
				break;
			};
		}
	}

	struct file_tag_ {} const file_tag;
	struct string_tag_ {} const string_tag;

	template<GLenum Enum> struct shader
	{
		typedef std::string string_type;
		static const GLenum ENUM = Enum;
		
		GLuint id_;
		GLuint id() { return id_; }

		shader(string_type const& n, file_tag_) 
		{
			std::fstream fs (n.c_str(), std::ios_base::in);

			if(fs.fail())
				throw -1;

			std::stringstream ss;

			while(!fs.eof())
			{
				string_type line;
				std::getline(fs,line);
				ss << line << '\n';
			}

			std::string str = ss.str();
			const char* cstr = str.c_str();

			id_ = glCreateShader(Enum);
			glShaderSource
				(	id_				// id to shader
				,	1				// number of strings
				,	&cstr			// pointer to string
				,	0 );			// length of string - null terminated = 0
			glCompileShader(id_);

			char buff[1024];
			GLsizei len = 0;
			glGetShaderInfoLog
				(	id_
				,	1024
				,	&len
				,	buff );
			buff[len] = 0;
			std::cout << n << " " << buff;

			if(glGetError() != GL_NO_ERROR)
			{
				glDeleteShader(id_);
				throw -1;
			}
		}

		shader(string_type const& n, string_tag_) 
		{
			const char* cstr = n.c_str();

			id_ = glCreateShader(Enum);
			glShaderSource
				(	id_				// id to shader
				,	1				// number of strings
				,	&cstr			// pointer to string
				,	0 );			// length of string - null terminated = 0
			glCompileShader(id_);

			char buff[1024];
			GLsizei len = 0;
			glGetShaderInfoLog
				(	id_
				,	1024
				,	&len
				,	buff );
			buff[len] = 0;
			std::cout << buff;

			if(glGetError() != GL_NO_ERROR)
			{
				glDeleteShader(id_);
				throw -1;
			}
		}
		~shader()
		{
			glDeleteShader(id_);
		}
	};

	typedef shader<GL_VERTEX_SHADER> vertex_shader;
	typedef shader<GL_FRAGMENT_SHADER> frag_shader;

	struct shader_program
	{
		typedef unsigned int uint;

		uint attribs_;
		uint attribs() { return attribs_; }
		void attribs(const uint a) { attribs_ = a; }

		GLuint id_;
		GLuint id() { return id_; }

		shader_program() 
			: 	attribs_(0)
		{
			id_ = glCreateProgram();
		}
		~shader_program()
		{
			glDeleteProgram(id_);
		}
	};

	struct bind_shader_
	{
		template<class Program, class Shader>
		void operator() (Program & program_, Shader & shader_) const
		{
			glAttachShader(program_.id(), shader_.id());
			test_gl_error("attach_shader");
		}
	} const bind_shader;

	struct bind_attribute_
	{
		typedef unsigned int uint;
		template<class Program, class String> uint operator()
			(	Program & prog
			,	String & name ) const
		{
			return (*this)(prog,name.c_str());
		}
		template<class Program> uint operator()
			(	Program & prog
			,	const char* name ) const
		{
			const uint a = prog.attribs(); 
			prog.attribs(a+1);
			glBindAttribLocation(prog.id(), a, name);
			test_gl_error("bind_attribute");
			return a;
		}
	} const bind_attribute;

	struct link_program_
	{
		template<class Program> void operator()(Program & program_) const
		{
			glLinkProgram(program_.id());
			GLint result;
			glGetProgramiv(program_.id(), GL_LINK_STATUS, &result);
			if(!result)
				throw -1;
		}
	} const link_program;

	struct use_program_
	{
		template<class Program> void operator () (Program & program_) const
		{
			glUseProgram(program_.id());
			test_gl_error("use program");
		}
	} const use_program;

	struct uniform
	{
		typedef std::string string_type;

		GLint id_;
		GLint id() const { return id_; }

		uniform(uniform const& u) : id_(u.id_) {}

		uniform(shader_program & program_, string_type const& n)
			:	id_(glGetUniformLocation(program_.id(), n.c_str())) 
		{
			test_gl_error("uniform constructor");
		}

		uniform(shader_program & program_, const char * n)
			:	id_(glGetUniformLocation(program_.id(), n)) 
		{
			test_gl_error("uniform constructor");
		}
		
		uniform() : id_(-1) {}

		uniform & operator = (uniform const& u) 
		{
			id_ = u.id_;
			return *this;
		}
	};

	struct set_uniform_
	{
		// 32bit real
		void handle(const GLint id, pudge::math::vec<4,float> & v) const
		{
			glUniform4f(id, v.x, v.y, v.z, v.w);
		}
		void handle(const GLint id, pudge::math::vec<3,float> & v) const
		{
			glUniform3f(id, v.x, v.y, v.z);
		}
		void handle(const GLint id, pudge::math::vec<2,float> & v) const
		{
			glUniform2f(id, v.x, v.y);
		}
		void handle(const GLint id, float & v) const
		{
			glUniform1f(id, v);
		}

		// 32bit int
		void handle(const GLint id, pudge::math::vec<4,long> & v) const
		{
			glUniform4i(id, v.x, v.y, v.z, v.w);
		}
		void handle(const GLint id, pudge::math::vec<3,long> & v) const
		{
			glUniform3i(id, v.x, v.y, v.z);
		}
		void handle(const GLint id, pudge::math::vec<2,long> & v) const
		{
			glUniform2i(id, v.x, v.y);
		}
		void handle(const GLint id, long & v) const
		{
			glUniform1i(id, v);
		}

		// matrix
		void handle(const GLint id, pudge::math::mat<float> & m) const
		{
			glUniformMatrix4fv(id, 16, false, &m.m11);
		}

		template<class Uniform, class Data>
		void operator () (Uniform & uniform_, Data & data_) const
		{
			handle(uniform_.id(), data_);
			test_gl_error("set_uniform");
		}
	} const set_uniform;
}}

#endif